﻿using System;
using System.Net;
using System.Windows;
using System.Reflection;
using System.Globalization;
using System.Text.RegularExpressions;
#if NETFX_CORE
using Dispatcher = Windows.UI.Core.CoreDispatcher;
using Windows.UI.Xaml;
using Windows.UI.Core;
#elif !__ANDROID__
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Threading;
#else
using System.Windows.Threading;
#endif

namespace UnitDriven.Commanding
{
	public class TestCommand : CommandBase
	{
		private Dispatcher _dispatcher;
		private Action _initialize;
		private Type _testType;
		private MethodInfo _testMethod;
		private object[] _arguments;
		private object _expectedResult;
		private string _expectedExceptionName;
		private string _expectedExceptionMessage;
		private MessageMatch _expectedExceptionMatchType;

		public TestCommand(
			Action initialize,
			Type testType,
			MethodInfo testMethod,
			object[] arguments = null,
			object expectedResult = null,
			string expectedExceptionName = null,
			string expectedExceptionMessage = null,
			MessageMatch match = MessageMatch.Exact)
		{
#if NETFX_CORE
            _dispatcher = Window.Current.Dispatcher;
#elif __ANDROID__
			_dispatcher = System.Windows.Threading.Dispatcher.CurrentDispatcher;
#else
			_dispatcher = Application.Current.RootVisual.Dispatcher;
#endif
			_initialize = initialize;
			_testType = testType;
			_testMethod = testMethod;
			_arguments = arguments;
			_expectedResult = expectedResult;
			_expectedExceptionName = expectedExceptionName;
			_expectedExceptionMessage = expectedExceptionMessage;
			_expectedExceptionMatchType = match;
		}

		protected override void Initialize()
		{
			_initialize();
			base.Initialize();
		}

        public override void Execute()
        {
#if NETFX_CORE
            var getContextMethod = _testType.GetTypeInfo().GetDeclaredMethod("GetContext");
            bool isAsync = (getContextMethod != null && typeof(UnitTestContext).GetTypeInfo().IsAssignableFrom(getContextMethod.ReturnType.GetTypeInfo()));
#else
			var getContextMethod = _testType.GetMethod("GetContext", BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
			bool isAsync = (getContextMethod != null && typeof(UnitTestContext).IsAssignableFrom(getContextMethod.ReturnType));
#endif
            bool expectsException = _expectedExceptionName != null;
			object instance = Activator.CreateInstance(_testType);
			InitializeInstance(instance);

			if (isAsync)
			{
				var context = (UnitTestContext)getContextMethod.Invoke(instance, null);
#if NETFX_CORE
				context.Completed += async (o, e) =>
#else
				context.Completed += (o, e) =>
#endif
                {
					TestResult result = TestResult.Indeterminate;
					string message = null;

					if (expectsException)
					{
						if (e.Status == TestResult.Fail &&
							e.Error != null &&
							e.Error.GetType().ToString() == this._expectedExceptionName)
						{
							result = VerifyExceptionMessage(e.Error.Message);
						}
						else result = TestResult.Fail;
					}
					else result = e.Status;

					if (result == TestResult.Fail && e.Error != null)
					{
						message = e.Error.Innermost().ToString();
					}
#if NETFX_CORE
                    await _dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                    {
                        CleanupInstance(instance);
                        OnComplete(new TestCompleteEventArgs(result, message, e.Error));
                    });
#else
					_dispatcher.BeginInvoke(() =>
					{
						CleanupInstance(instance);
						OnComplete(new TestCompleteEventArgs(result, message, e.Error));
					});
#endif
				};

				try
				{
					context.SetTimeout(this.Timeout);
					var returnValue = _testMethod.Invoke(instance, _arguments);
					if (returnValue != null)
					{
						CleanupInstance(instance);
						OnComplete(new TestCompleteEventArgs(TestResult.Fail, "Async tests must not have return values.", null));
					}
				}
				catch (Exception ex)
				{
					TestResult result = TestResult.Fail;
					string message = ex.Innermost().ToString();
					CleanupInstance(instance);
					OnComplete(new TestCompleteEventArgs(result, message, ex));
				}
			}
			else
			{
				TestResult result = TestResult.Indeterminate;
				string message = null;
				Exception error = null;

				try
				{
					var returnResult = _testMethod.Invoke(instance, _arguments);

					if ((_expectedResult != null && returnResult != null && !_expectedResult.Equals(returnResult)) ||
						(_expectedResult == null && returnResult != null) ||
						(_expectedResult != null && returnResult == null))
					{
						result = TestResult.Fail;
					}

					if (expectsException)
					{
						result = TestResult.Fail;
					}
					else
					{
						result = TestResult.Success;
					}
				}
				catch (Exception ex)
				{
					if (expectsException && _expectedExceptionName == ex.InnerException.GetType().ToString())
					{
						result = VerifyExceptionMessage(ex.Message);
					}
					else
					{
						result = TestResult.Fail;
						message = ex.Innermost().ToString();
						error = ex;
					}
				}
				finally
				{
					CleanupInstance(instance);
#if NETFX_CORE
                    this._dispatcher.RunAsync(
                        CoreDispatcherPriority.Normal,
                        () => OnComplete(new TestCompleteEventArgs(result, message, error)))
                        .AsTask();
#else
                    OnComplete(new TestCompleteEventArgs(result, message, error));
#endif
                }
			}
		}

		private TestResult VerifyExceptionMessage(string message)
		{
			TestResult result = TestResult.Success;
			if (this._expectedExceptionMessage != null)
			{
				switch (this._expectedExceptionMatchType)
				{
					case MessageMatch.Exact:
                        result = this._expectedExceptionMessage == message ? TestResult.Success : TestResult.Fail;
						break;
					case MessageMatch.Contains:
                        result = message.Contains(this._expectedExceptionMessage) ? TestResult.Success : TestResult.Fail;
						break;
					case MessageMatch.StartsWith:
                        result = message.StartsWith(this._expectedExceptionMessage) ? TestResult.Success : TestResult.Fail;
						break;
					case MessageMatch.Regex:
						var regex = new Regex(this._expectedExceptionMessage);
                        result = regex.IsMatch(message) ? TestResult.Success : TestResult.Fail;
						break;
				}
			}

			return result;
		}

		private void InitializeInstance(object instance)
		{
#if NETFX_CORE
            var methods = _testType.GetTypeInfo().DeclaredMethods;
#else
			var methods = _testType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);
#endif
			foreach (MethodInfo m in methods)
			{
				if (m.IsDefined(typeof(TestInitializeAttribute), true))
				{
					m.Invoke(instance, null);
					break;
				}
			}
		}

		private void CleanupInstance(object instance)
        {
#if NETFX_CORE
            var methods = _testType.GetTypeInfo().DeclaredMethods;
#else
			var methods = _testType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);
#endif
			foreach (MethodInfo m in methods)
			{
				if (m.IsDefined(typeof(TestCleanupAttribute), true))
				{
					m.Invoke(instance, null);
					break;
				}
			}
		}
	}

	public class TestCompleteEventArgs : CommandCompleteEventArgs
	{
		public string Message { get; protected set; }
		public TestResult Result { get; protected set; }

		public TestCompleteEventArgs(TestResult result, string message, Exception error)
			: base(error)
		{
			Result = result;
			Message = message;
		}
	}
}
